using System;
using System.Collections.Generic;

namespace Common.Core.Caching
{
	internal class EntityCache<EntityType, CachingKeyType> : IEntityCache<EntityType, CachingKeyType> where EntityType : class
	{
		private readonly int capacity;
		private readonly bool recaching;
		private readonly Cache cache = new Cache();
		private readonly object sync = new object();

		public EntityCache() : this(int.MaxValue, true)
		{
		}

		public EntityCache(int capacity, bool recaching)
		{
			if(capacity < 2)
			{
				throw new ArgumentException("Cache capacity should always exceed 2.", "capcity");
			}

			this.capacity = capacity;
			this.recaching = recaching;
		}

		public EntityType this[CachingKeyType key]
		{
			get
			{
				lock (sync)
				{
					if (!cache.ContainsKey(key))
					{
						return null;
					}

					var node = CreateNode(key, cache[key]);

					PutNodeAtTheBegining(node);

					return node.Node.Value;
				}
			}
			set
			{
				lock (sync)
				{
					if (cache.ContainsKey(key))
					{
						cache.Remove(key);
						Register(value, key);
					}
					else
					{
						Register(value, key);
					}
				}
			}
		}

		private void Register(EntityType entity, CachingKeyType key)
		{
			if (cache.ContainsKey(key) && !recaching)
			{
				throw new DuplicateCachingEntryException();
			}

			var node = CreateNode(key, entity);

			cache.Add(key, entity);

			PutNodeAtTheBegining(node);

			if (capacity < cache.Count)
			{
				RemoveLastElement();
			}
		}

		private CachingNode<EntityType, CachingKeyType> CreateNode(CachingKeyType key, EntityType entity)
		{
			return new CachingNode<EntityType, CachingKeyType>
			       	{
			       		Node = new KeyValuePair<CachingKeyType, EntityType>(key, entity),
			       		NextNode = cache.firstNode,
			       		PrevNode = null
			       	};
		}

		private void PutNodeAtTheBegining(ICachingNode<EntityType, CachingKeyType> node)
		{
			if (cache.firstNode != null)
			{
				cache.firstNode.NextNode = node;
			}

			cache.firstNode = node;

			if (cache.lastNode == null)
			{
				cache.lastNode = cache.firstNode;
			}
		}

		private void RemoveLastElement()
		{
			cache.lastNode.NextNode.PrevNode = null;
			cache.Remove(cache.lastNode.Node.Key);
			cache.lastNode = cache.lastNode.NextNode;
		}


		class Cache : Dictionary<CachingKeyType, EntityType>
		{
			public ICachingNode<EntityType, CachingKeyType> firstNode;
			public ICachingNode<EntityType, CachingKeyType> lastNode;
		}
	}
}